// implements game logic; code is shared between client and server

#include "main.h"
#include "logic.h"
#include "world/actor.h"
#include "world/action.h"
#include "world/effect.h"
#include "world/map.h"
#include "game.h"
#include "network.h"
#include "resources/resources.h"
#include "util/log.h"
#include "util/mt.h"

void (*damage_handler) (int time, Coord where, int attacker, int victim, int weapon, int damage) = NULL;
void (*effect_handler) (Effect* effect) = NULL;

static bool is_in_bounds (Map* map, int x, int y);
static bool tile_is_obstructed (Map* map, int x, int y, int z);
static bool can_interact (Map* map, Actor* character, Direction direction);
static void execute_action (Action* action);
static void action_begin (Action* action);
static void action_finished (Action* action);
static Action get_force_followup_action (Map* map, Action* action);
static Action get_default_followup_action (Action* action);
static void logic_monster_decide (Actor* monster, Map* map, int time);
static void actor_enter_tile (Actor* actor, Tile* tile);
static void actor_activate_tile (Actor* actor, Tile* tile);
static void actor_exit_tile (Actor* actor, Tile* tile);

static void dummy_effect_handler (Effect* effect)
{
	assert(!"no effect handler");
}

void logic_reset ()
{
	damage_handler = NULL;
	effect_handler = &dummy_effect_handler;
	for (int i = 0; i < the_game.map.actor.count; i++) {
		Actor* actor = map_get_actor (&the_game.map, i);
		action_free (&actor->action);
		action_free (&actor->intention);
		actor->action = NO_ACTION(actor->id);
		actor->intention = NO_ACTION(actor->id);
	}
}

// Returns the time when this actor can do another action
int time_asap (int actor_id)
{
	assert((actor_id >= 0) && (actor_id < the_game.map.actor.count));
	
	Actor* actor = map_get_actor (&the_game.map, actor_id);
	return the_game.game_time + actor->action.cooldown;
}

/*
 * Updates the action of an actor. a is the action, wa is the
 * corresponding intention, time_asap is the next opportunity
 * in ticks to execute a new action.
 */
static void logic_update_action (Action* a, Action* wa, int time_asap)
{
	Actor* actor = map_get_actor (&the_game.map, a->actor);
	
	execute_action (a);
	
	while ((a->cooldown <= 0) || ((wa->type != ACTION_NONE) &&
			 (wa->start_time <= the_game.game_time))) {
		action_finished (a);
		Action next_action = get_force_followup_action (&the_game.map, a);
		if (next_action.type == ACTION_NONE) {
			if (wa->type == ACTION_NONE) {
				next_action = get_default_followup_action (a);
			} else {
				next_action = *wa;
			}
		}
			
		if (!action_is_valid (&the_game.map, &next_action)) {
			Action trash = next_action;
			if (next_action.type == ACTION_MOVE) {
				if (can_interact (&the_game.map, actor, actor->dir)) {
					Coord obj_coord = direction_apply (actor->coord, actor->dir, 1);
					int target = TILEC (&the_game.map, obj_coord).object->index;
					next_action = action_alloc_interact (a->actor, time_asap, target);
				} else {
					next_action = action_alloc_halt (a->actor, time_asap);
				}
			} else {
				next_action = NO_ACTION(a->actor);
			}
			action_free (&trash);
		}
			
		action_free (a);
		*a = next_action;
		*wa = get_default_followup_action (a);
		action_begin (a);
		
		if (a->type == ACTION_NONE) break;
	}
}

static void logic_update_actor (Actor* actor)
{
	int asap = time_asap (actor->id);
	logic_update_action (&actor->action, &actor->intention, asap);
	
	if (actor->cooldown > 0) actor->cooldown--;
	
	if ((actor->type == ACTOR_MONSTER) && actor_can_act (actor)) {
		logic_monster_decide (actor, &the_game.map, asap);
	}
}

void logic_update (Map* map)
{
	for (int i = 0; i < map->actor.count; i++) {
		logic_update_actor (map_get_actor (map, i));
	}
	
	the_game.game_time++;
}

void logic_intend (Action* action)
{
	assert((action->actor >= 0) && (action->actor < the_game.map.actor.count));
	
	Actor* actor = map_get_actor (&the_game.map, action->actor);
	actor->intention = *action;
}

static bool is_in_bounds (Map* map, int x, int y)
{
	return (x >= 0) && (x < map->width) && (y >= 0) && (y < map->height);
}

// obstructions handled until now:
// - walls
// - other players
static bool tile_is_obstructed (Map* map, int x, int y, int z)
{
	assert(map != NULL);
	assert((x >= 0) && (x < map->width));
	assert((y >= 0) && (y < map->height));
	assert((z >= 0) && (z < map->layers));
	
	// TODO: handle different kinds of obstruction, blocks_monsters etc.
	if (TILE(map,x,y,z).blocks_players) return true;
	if (TILE(map,x,y,z).has_object) return true;
	
	Coord c = {x, y, z};
	for (int i = 0; i < map->actor.count; i++) {
		Actor* actor = map_get_actor (map, i);
		if (COORDS_EQUAL (actor->coord, c)) return true;
	}
	
	return false;
}

static bool can_interact (Map* map, Actor* character, Direction direction)
{
	assert(map != NULL);
	assert(character != NULL);
	assert((direction == DIR_LEFT) || (direction == DIR_RIGHT) ||
	       (direction == DIR_UP) || (direction == DIR_DOWN));
	assert(character->type == ACTOR_CHARACTER);
	
	Coord obj_coord = direction_apply (character->coord, direction, 1);
	Tile* target = &TILEC(map,obj_coord);
	if (!target->has_object) return false;
	
	MapObject* object = target->object;
	Direction object_dir = object->orientation;
	if (object_dir != DIR_NONE) {
		// verify that player is approaching from the right angle
		if (direction_primary (object->coord, character->coord) != object_dir) {
			return false;
		}
	}
	
	return true;
}

static void logic_interact (Actor* actor, int target)
{
	assert(actor != NULL);
	assert(actor->type == ACTOR_CHARACTER); // monsters cannot interact for now
	assert((target >= 0) && (target < the_game.map.object.count));
	
	MapObject* object = map_get_object (&the_game.map, target);
	
	switch (object->type) {
		
	case OBJ_PICKUP:
		break; // not implemented
	
	case OBJ_TREASURE_GOLD:
		actor->character->gold += object->treasure_gold.amount;
		object->state = 1;
		Effect effect;
		effect_init_gold (&effect, object->treasure_gold.amount, object->coord);
		effect_handler (&effect);
		break;
	
	case OBJ_TREASURE_ITEM:
		break; // not implemented
		
	case OBJ_LEVER:
		break; // not implemented
		
	case OBJ_SOKOBOX:
		break; // not implemented
		
	default:
	case OBJ_OBSTACLE:
		break; // do nothing
		
	}
}

static void execute_action (Action* action)
{
	assert(action != NULL);
	assert((action->actor >= 0) && (action->actor < the_game.map.actor.count));
	
	Actor* actor = map_get_actor (&the_game.map, action->actor);
	int cooldown_passed;
	int frame_time = 0;     // These 2 values are used in deciding
	int frame_duration = 0; // whether to advance an actor's anim frame
	
	switch (action->type) {
	
	case ACTION_RESPAWN:
	case ACTION_TRAVERSE:
		break;
	
	case ACTION_NONE:
	case ACTION_TURN:
	case ACTION_HALT:
		if (actor->type == ACTOR_MONSTER) {
			frame_time = the_game.game_time;
			frame_duration = (FPS/6);
		}
		break;
	
	case ACTION_MOVE:
		frame_time = the_game.game_time;
		frame_duration = 4;
		break;
	
	case ACTION_DIE:
		frame_time = the_game.game_time;
		frame_duration = (FPS/10);
		break;
	
	case ACTION_ATTACK_CLOSE:
		cooldown_passed = items[action->cattack->weapon].cooldown - action->cooldown; // TODO: this cooldown system will change
		frame_time = cooldown_passed;
		frame_duration = (FPS/10);
		break;
	
	case ACTION_ATTACK_RANGE:
		log_debug ("logic: execute ranged attack with weapon %d", action->rattack->weapon);
		cooldown_passed = items[action->rattack->weapon].cooldown - action->cooldown; // TODO: this cooldown system will change
		frame_time = cooldown_passed;
		frame_duration = (FPS/10);
		break;
	
	case ACTION_INTERACT:;
		#if TOWERHACK_DEBUG // DEBUG: remove object on interact
		MapObject* object = map_get_object (&the_game.map, action->interact->target);
		Tile* obj_tile = &TILEC (&the_game.map, object->coord);
		obj_tile->has_object = false;
		obj_tile->object = NULL;
		#endif
		break;
	
	default:
		#if TOWERHACK_DEBUG
		log_debug ("logic: cannot execute action of type %d, ignored!", action->type);
		#else
		assert(!"Cannot execute action of unknown type.");
		#endif
		break;
	
	}
	
	if ((frame_duration > 0) && ((frame_time%frame_duration) == (frame_duration-1))) {
		actor->gfx.frame++;
	}
	action->cooldown--;
}

static int calc_damage (int attacker, int victim, InvSlot inv_weapon)
{
	assert((attacker >= 0) && (attacker < the_game.map.actor.count));
	assert((victim >= 0) && (victim < the_game.map.actor.count));
	
	Actor* a_actor = map_get_actor (&the_game.map, attacker);
	Actor* v_actor = map_get_actor (&the_game.map, victim);

	assert((a_actor->type == ACTOR_CHARACTER) || (a_actor->type == ACTOR_MONSTER));
	assert((v_actor->type == ACTOR_CHARACTER) || (v_actor->type == ACTOR_MONSTER));

	Item* weapon;
	Item* armor;
	
	if (a_actor->type == ACTOR_CHARACTER) {
		weapon = &items[a_actor->character->inventory.slot[inv_weapon]];
	} else {
		weapon = &items[0];
	}
	
	if (v_actor->type == ACTOR_CHARACTER) {
		armor = &items[v_actor->character->inventory.slot[INV_ARMOR]];
	} else {
		armor = &items[0];
	}
	
	int phys_d = weapon->physical_damage;
	int mag_d  = weapon->magic_damage;
	int imm_d  = weapon->immaterial_damage;

	double phys_r = armor->physical_reduce;
	double mag_r  = armor->magic_reduce;
	double imm_r  = armor->immaterial_reduce;

	return phys_r * phys_d + mag_r * mag_d + imm_r * imm_d;
}

static void action_begin (Action* action)
{
	assert(action != NULL);
	assert((action->actor >= 0) && (action->actor < the_game.map.actor.count));
	
	if (action->type == ACTION_NONE) return;
	
	Actor* actor = map_get_actor (&the_game.map, action->actor);
	Tile* tile = NULL;
	
	// NOTE: action->location and dir are deprecated.
	// Updating the pos & dir on every action helps with network sync (?)
	// actor->coord = action->location;
	// actor->dir = action->dir;
		
	switch (action->type) {
	
	case ACTION_NONE:
	case ACTION_HALT:
	case ACTION_RESPAWN:
		break;
		
	case ACTION_TURN:
		actor->dir = action->turn->direction;
		break;
	
	case ACTION_MOVE:
		tile = &TILEC (&the_game.map, actor->coord);
		actor_exit_tile (actor, tile);
		
		actor->coord = direction_apply (actor->coord, actor->dir, 1);
		
		tile = &TILEC (&the_game.map, actor->coord);
		actor_enter_tile (actor, tile);
		break;
	
	case ACTION_INTERACT:
		logic_interact (actor, action->interact->target);
		break;
	
	case ACTION_TRAVERSE:
		tile = &TILEC (&the_game.map, actor->coord);
		actor_exit_tile (actor, tile);
		break;
	
	case ACTION_ATTACK_CLOSE:
		actor->gfx.frame = 0;
		Coord to = direction_apply (actor->coord, actor->dir, 1);
		
		BITMAP* weapon_image = resources_get_weapon (action->cattack->weapon);
		Effect effect;
		effect_init_weapon (&effect, weapon_image, actor->dir, actor->coord);
		effect_handler (&effect);
		
		for (int i = 0; i < the_game.map.actor.count; i++) {
			Actor* victim = map_get_actor (&the_game.map, i);
			if (victim->type == ACTOR_INVALID) continue;
			if (COORDS_EQUAL (victim->coord, to)) {
				int attacker_id = action->actor;
				int victim_id = victim->id;
				int damage = calc_damage (attacker_id, victim_id, INV_WEAPON_CLOSE);
				
				if (damage_handler != NULL) {
					damage_handler (the_game.game_time + (FPS/10), to, attacker_id,
					                victim_id, action->cattack->weapon, damage);
				}
			}
		}
		break;

	case ACTION_ATTACK_RANGE:
		actor->gfx.frame = 0;
		// TODO: deal damage
		break;

	case ACTION_DIE:
		actor->gfx.frame = 0;
		break;

	default:
		assert(!"cannot begin action of unhandled type");
	
	}
}

static void action_finished (Action* action)
{
	assert(action != NULL);
	assert((action->actor >= 0) && (action->actor < the_game.map.actor.count));
	
	if (action->type == ACTION_NONE) return;
	
	Actor* actor = map_get_actor (&the_game.map, action->actor);
	Tile* tile = NULL;

	switch (action->type) {

	case ACTION_MOVE:
		tile = &TILEC (&the_game.map, actor->coord);
		actor_activate_tile (actor, tile);
		break;
	
	case ACTION_NONE:
	case ACTION_TURN:
	case ACTION_INTERACT:
	case ACTION_HALT:
		break;

	case ACTION_TRAVERSE:
		// Monsters do not traverse
		assert(actor->type == ACTOR_CHARACTER);
		
		#if TOWERHACK_DEBUG
		log_debug ("Teleport from (%d,%d,%d) to (%d,%d,%d)", XYZ (actor->coord),
		           XYZ (action->traverse->target));
		#endif
		
		actor->coord = action->traverse->target;
		tile = &TILEC (&the_game.map, actor->coord);
		actor_enter_tile (actor, tile);
		actor_activate_tile (actor, tile);
		
		actor->intention = NO_ACTION(action->actor); // any moves we had in mind are now invalid
		break;

	case ACTION_ATTACK_CLOSE:
	case ACTION_ATTACK_RANGE:
	case ACTION_DIE:
		actor->gfx.frame = 0;
		break;

	case ACTION_RESPAWN:
		actor_respawn (actor);
		break;
		
	default:
		#if TOWERHACK_DEBUG
		log_debug ("logic: cannot end action of type %d, ignored!", action->type);
		#else
		assert(!"cannot end action of unknown type.");
		#endif
		break;

	}
}

bool action_is_valid (Map* map, Action* action)
{
	assert(map != NULL);
	assert(action != NULL);
	assert((action->actor >= 0) && (action->actor < the_game.map.actor.count));
	
	switch (action->type) {
	
	case ACTION_TURN: // can always turn
		return true;
	
	case ACTION_MOVE:;
		Actor* actor = map_get_actor (&the_game.map, action->actor);
		Coord c = direction_apply (actor->coord, actor->dir, 1);
		return is_in_bounds(map, c.x, c.y) &&
		       !tile_is_obstructed (map, XYZ(c));

	case ACTION_TRAVERSE: // to be validated in the future?
		return true;

	default:
		return true;

	}
	
	return true;
}

static Action get_force_followup_action (Map* map, Action* action)
{
	assert(map != NULL);
	assert(action != NULL);
	assert((action->actor >= 0) && (action->actor < the_game.map.actor.count));
	
	if (action->type == ACTION_MOVE) {
		Actor* actor = map_get_actor (&the_game.map, action->actor);
		int index = TILEC_INDEX (map, actor->coord);
		Tile* target = &map->tile[index];
		
		if (target->type == TILE_EXTENDED) {
			TileExtension* port = target->extension;
			assert(port != NULL);
			
			switch (port->type) {
				case TE_STAIRS:
					return action_alloc_traverse (action->actor, the_game.game_time, 
															port->stairs.target);

				case TE_TELEPORTER:
					return action_alloc_traverse (action->actor, the_game.game_time, 
															port->teleporter.target);

				default:
					break;
			}
		}
		
	} else
	if (action->type == ACTION_TURN) {
		return action_alloc_move (action->actor, the_game.game_time);
	} else
	if (action->type == ACTION_DIE) {
		return action_alloc_respawn (action->actor, the_game.game_time);
	}
	
	return NO_ACTION (action->actor);
}

static Action get_default_followup_action (Action* action)
{
	if (action->type == ACTION_MOVE) {
		return action_alloc_move (action->actor, action->start_time + COOLDOWN_MOVE);
	}
	
	return NO_ACTION (action->actor);
}

/*
 * This function sets the intention of the monster based on 
 * an evaluation of its surroundings. It may for example decide
 * to attack the player or maybe do nothing at all.
 * Players who are in the array but inactive/nonexistant are 
 * tolerated.
 * time is the "next opportunity" time for an action from this monster.
 */
static void logic_monster_decide (Actor* monster, Map* map, int time)
{
	assert(monster != NULL);
	assert(map != NULL);
	assert(monster->type == ACTOR_MONSTER);
	
	#if 0 // TOWERHACK_DEBUG
	// Disable distracting monster actions
	monster->intention = action_alloc_halt (monster->id, 999999);
	monster->cooldown = 9999999;
	#else
	int target_distance = MONSTER_AGGRESSION_RADIUS;
	Actor* targets[MAX_PLAYERS]; // Players which are eligible for attack
	int target_count = 0;

	// Find nearest players and fill targets array
	for (int i = 0; i < map->actor.count; i++) {
		Actor* target = map_get_actor (map, i);
		if (target->type == ACTOR_CHARACTER) {
			if (target->coord.z != monster->coord.z) continue; // Only atack targets on same level
			
			int dist = abs (target->coord.x - monster->coord.x) + abs (target->coord.y - monster->coord.y);
			if (dist < target_distance) {
				target_distance = dist;
				targets[0] = target;
				target_count = 1;
			} else
			if (dist == target_distance) {
				targets[target_count] = target;
				target_count++;
			}
		}
	}
	
	if (target_count == 0) {
		monster->intention = action_alloc_halt (monster->id, time);
	} else {
		int index = (target_count == 1) ? 0 : mt_randint (0, target_count);
		Actor* target = targets[index];
		Direction move_dir = direction_primary (monster->coord, target->coord);
		if ((target_distance == 1) &&
		    (monster->dir == move_dir) &&
		    actor_can_attack (monster))
		{
			monster->intention = action_alloc_closeattack (monster->id, time, 0);
		} else 
		if (monster->dir != move_dir) {
			monster->intention = action_alloc_turn (monster->id, time, move_dir);
		}
	}
	#endif
}

static void actor_enter_tile (Actor* actor, Tile* tile)
{
	tile->actor = actor->id;
	tile->has_actor = true;
}

static void actor_activate_tile (Actor* actor, Tile* tile)
{
}

static void actor_exit_tile (Actor* actor, Tile* tile)
{
	tile->actor = -1;
	tile->has_actor = false;
}
